local _, Addon = ...
local Dominos = LibStub("AceAddon-3.0"):GetAddon("Dominos")
local LSM = LibStub("LibSharedMedia-3.0")
local L = LibStub("AceLocale-3.0"):GetLocale("Dominos-CastBar")

-- local aliases for some globals
local GetSpellInfo = _G.GetSpellInfo
local GetTime = _G.GetTime

local UnitCastingInfo = _G.UnitCastingInfo or _G.CastingInfo
local UnitChannelInfo = _G.UnitChannelInfo or _G.ChannelInfo

local IsHarmfulSpell = _G.IsHarmfulSpell
local IsHelpfulSpell = _G.IsHelpfulSpell

local ICON_OVERRIDES = {
    -- replace samwise with cog
    [136235] = 136243
}

local CAST_BAR_COLORS = {
    default = {1, 0.7, 0},
    failed = {1, 0, 0},
    harm = {0.63, 0.36, 0.94},
    help = {0.31, 0.78, 0.47},
    spell = {0, 1, 0},
    uninterruptible = {0.63, 0.63, 0.63}
}

local LATENCY_BAR_ALPHA = 0.5

local function GetSpellReaction(spellID)
    local name = GetSpellInfo(spellID)
    if name then
        if IsHelpfulSpell(name) then
            return "help"
        end

        if IsHarmfulSpell(name) then
            return "harm"
        end
    end

    return "default"
end

local CastBar = Dominos:CreateClass("Frame", Dominos.Frame)

function CastBar:New(id, units, ...)
    local bar = CastBar.proto.New(self, id, ...)

    bar.units = type(units) == "table" and units or {units}
    bar:Layout()
    bar:RegisterEvents()

    return bar
end

CastBar:Extend("OnCreate", function(self)
    self:SetScript("OnEvent", self.OnEvent)

    self.props = {}

    self.timer = CreateFrame("Frame", nil, self, "DominosTimerBarTemplate")
end)

CastBar:Extend("OnRelease", function(self)
    self:UnregisterAllEvents()
    LSM.UnregisterAllCallbacks(self)
end)

CastBar:Extend("OnLoadSettings", function(self)
    if not self.sets.display then
        self.sets.display = {icon = false, time = true, border = true, latency = true}
    end

    self:SetProperty("font", self:GetFontID())
    self:SetProperty("texture", self:GetTextureID())
    self:SetProperty("reaction", "neutral")
end)

function CastBar:GetDisplayName()
    local L = LibStub("AceLocale-3.0"):GetLocale("Dominos-CastBar")

    return L.CastBarDisplayName
end

function CastBar:GetDefaults()
    return {
        point = "CENTER",
        x = 0,
        y = 30,
        padW = 1,
        padH = 1,
        texture = "blizzard",
        font = "Friz Quadrata TT",

        useSpellReactionColors = true,

        -- default to the spell queue window for latency padding
        latencyPadding = tonumber(GetCVar("SpellQueueWindow")),

        displayLayer = 'HIGH',

        display = {icon = false, time = true, border = true, latency = true, spark = true}
    }
end

--------------------------------------------------------------------------------
-- Game Events
--------------------------------------------------------------------------------

function CastBar:OnEvent(event, ...)
    local func = self[event]

    if func then
        func(self, event, ...)
    end
end

function CastBar:RegisterEvents()
    local function registerUnitEvents(...)
        self:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_START", ...)
        self:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_UPDATE", ...)
        self:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_STOP", ...)

        self:RegisterUnitEvent("UNIT_SPELLCAST_START", ...)
        self:RegisterUnitEvent("UNIT_SPELLCAST_STOP", ...)
        self:RegisterUnitEvent("UNIT_SPELLCAST_FAILED", ...)
        self:RegisterUnitEvent("UNIT_SPELLCAST_FAILED_QUIET", ...)

        self:RegisterUnitEvent("UNIT_SPELLCAST_INTERRUPTED", ...)
        self:RegisterUnitEvent("UNIT_SPELLCAST_DELAYED", ...)
    end

    registerUnitEvents(unpack(self.units))
    LSM.RegisterCallback(self, "LibSharedMedia_Registered")
end

-- channeling events
function CastBar:UNIT_SPELLCAST_CHANNEL_START(event, unit, castID, spellID)
    castID = castID or spellID

    self:SetProperty("castID", castID)
    self:SetProperty("unit", unit)

    self:UpdateChanneling()
end

function CastBar:UNIT_SPELLCAST_CHANNEL_UPDATE(event, unit, castID, spellID)
    castID = castID or spellID

    if castID ~= self:GetProperty("castID") then
        return
    end

    self:UpdateChanneling()
end

function CastBar:UNIT_SPELLCAST_CHANNEL_STOP(event, unit, castID, spellID)
    castID = castID or spellID

    if castID ~= self:GetProperty("castID") then
        return
    end

    self:SetProperty("state", "stopped")
end

function CastBar:UNIT_SPELLCAST_START(event, unit, castID, spellID)
    if castID == nil then
        return
    end

    self:SetProperty("castID", castID)
    self:SetProperty("unit", unit)

    self:UpdateCasting()
end

function CastBar:UNIT_SPELLCAST_STOP(event, unit, castID, spellID)
    if castID ~= self:GetProperty("castID") then
        return
    end

    self:SetProperty("state", "stopped")
end

function CastBar:UNIT_SPELLCAST_FAILED(event, unit, castID, spellID)
    if castID ~= self:GetProperty("castID") then
        return
    end

    self:SetProperty("label", _G.FAILED)
    self:SetProperty("state", "failed")
end

CastBar.UNIT_SPELLCAST_FAILED_QUIET = CastBar.UNIT_SPELLCAST_FAILED

function CastBar:UNIT_SPELLCAST_INTERRUPTED(event, unit, castID, spellID)
    if castID ~= self:GetProperty("castID") then
        return
    end

    self:SetProperty("label", _G.INTERRUPTED)
    self:SetProperty("state", "interrupted")
end

function CastBar:UNIT_SPELLCAST_DELAYED(event, unit, castID, spellID)
    if castID ~= self:GetProperty("castID") then
        return
    end

    self:UpdateCasting()
end

--------------------------------------------------------------------------------
-- Addon Events
--------------------------------------------------------------------------------

function CastBar:LibSharedMedia_Registered(event, mediaType, key)
    if mediaType == LSM.MediaType.STATUSBAR and key == self:GetTextureID() then
        self:texture_update(key)
    elseif mediaType == LSM.MediaType.FONT and key == self:GetFontID() then
        self:font_update(key)
    end
end

--------------------------------------------------------------------------------
-- Cast Bar Property Events
--------------------------------------------------------------------------------

function CastBar:state_update(state)
    if state == "interrupted" or state == "failed" then
        self:UpdateColor()
        self:Stop()
    elseif state == "stopped" then
        self:Stop()
    else
        self:UpdateColor()
    end
end

function CastBar:label_update(text)
    self.timer:SetLabel(text)
end

function CastBar:icon_update(texture)
    self.timer:SetIcon(texture and ICON_OVERRIDES[texture] or texture)
end

function CastBar:reaction_update(reaction)
    self:UpdateColor()
end

function CastBar:spell_update(spellID)
    local reaction = GetSpellReaction(spellID)

    self:SetProperty("reaction", reaction)
end

function CastBar:uninterruptible_update(uninterruptible)
    self:UpdateColor()
end

function CastBar:font_update(fontID)
    self.timer:SetFont(fontID)
end

function CastBar:texture_update(textureID)
    self.timer:SetTexture(textureID)
end

--------------------------------------------------------------------------------
-- Cast Bar Methods
--------------------------------------------------------------------------------

function CastBar:SetProperty(key, value)
    local prev = self.props[key]

    if prev ~= value then
        self.props[key] = value

        local func = self[key .. "_update"]
        if func then
            func(self, value, prev)
        end
    end
end

function CastBar:GetProperty(key)
    return self.props[key]
end

function CastBar:Layout()
    self:TrySetSize(self:GetDesiredWidth(), self:GetDesiredHeight())

    self.timer:SetPadding(self:GetPadding())

    self.timer:SetShowIcon(self:Displaying("icon"))

    self.timer:SetShowText(self:Displaying("time"))

    self.timer:SetShowBorder(self:Displaying("border"))

    self.timer:SetShowLatency(self:Displaying("latency"))
    self.timer:SetLatencyPadding(self:GetLatencyPadding())

    self.timer:SetShowSpark(self:Displaying("spark"))
end

function CastBar:UpdateChanneling()
    local name, text, texture, startTimeMS, endTimeMS, _, notInterruptible, spellID =
        UnitChannelInfo(self:GetProperty("unit"))

    if name then
        self:SetProperty("state", "channeling")
        self:SetProperty("label", name or text)
        self:SetProperty("icon", texture)
        self:SetProperty("spell", spellID)
        self:SetProperty("uninterruptible", notInterruptible)

        self.timer:SetCountdown(true)
        self.timer:SetShowLatency(false)

        local time = GetTime()
        local startTime = startTimeMS / 1000
        local endTime = endTimeMS / 1000

        self.timer:Start(endTime - time, 0, endTime - startTime)

        return true
    end

    return false
end

function CastBar:UpdateCasting()
    local name, text, texture, startTimeMS, endTimeMS, _, _, notInterruptible, spellID = UnitCastingInfo(
                                                                                             self:GetProperty("unit"))

    if name then
        self:SetProperty("state", "casting")
        self:SetProperty("label", text)
        self:SetProperty("icon", texture)
        self:SetProperty("spell", spellID)
        self:SetProperty("uninterruptible", notInterruptible)

        self.timer:SetCountdown(false)
        self.timer:SetShowLatency(self:Displaying("latency"))

        local time = GetTime()
        local startTime = startTimeMS / 1000
        local endTime = endTimeMS / 1000

        self.timer:Start(time - startTime, 0, endTime - startTime)

        return true
    end

    return false
end

local function getLatencyColor(r, g, b)
    return 1 - r, 1 - g, 1 - b, LATENCY_BAR_ALPHA
end

function CastBar:GetColorID()
    local state = self:GetProperty("state")
    if state == "failed" or state == "interrupted" then
        return "failed"
    end

    local reaction = self:GetProperty("reaction")

    if self:UseSpellReactionColors() then
        if reaction == "help" then
            return "help"
        end

        if reaction == "harm" then
            if self:GetProperty("uninterruptible") then
                return "uninterruptible"
            end

            return "harm"
        end
    else
        if reaction == "help" then
            return "spell"
        end

        if reaction == "harm" then
            if self:GetProperty("uninterruptible") then
                return "uninterruptible"
            end

            return "spell"
        end
    end

    return "default"
end

function CastBar:UpdateColor()
    local color = self:GetColorID()
    local r, g, b = unpack(CAST_BAR_COLORS[self:GetColorID()])

    self.timer.statusBar:SetStatusBarColor(r, g, b)

    if color == "failed" then
        self.timer.latencyBar:SetColorTexture(0, 0, 0, 0)
    else
        self.timer.latencyBar:SetColorTexture(getLatencyColor(r, g, b))
    end
end

function CastBar:Stop()
    self.timer:Stop()
end

function CastBar:SetupDemo()
    local spellID = self:GetRandomSpellID()
    local name, rank, icon, castTime = GetSpellInfo(spellID)

    -- use the spell cast time if we have it, otherwise set a default one
    -- of a few seconds
    if not (castTime and castTime > 0) then
        castTime = 3
    else
        castTime = castTime / 1000
    end

    self:SetProperty("state", "demo")
    self:SetProperty("label", name)
    self:SetProperty("icon", icon)
    self:SetProperty("spell", spellID)
    self:SetProperty("reaction", GetSpellReaction(spellID))
    self:SetProperty("uninterruptible", nil)

    self.timer:SetCountdown(false)
    self.timer:SetShowLatency(self:Displaying("latency"))
    self.timer:Start(0, 0, castTime)

    -- loop the demo if it is still visible
    C_Timer.After(castTime, function()
        if self.menuShown and self:GetProperty("state") == "demo" then
            self:SetupDemo()
        end
    end)
end

function CastBar:GetRandomSpellID()
    local spells = {}

    for i = 1, GetNumSpellTabs() do
        local _, _, offset, numSpells = GetSpellTabInfo(i)

        for j = offset, (offset + numSpells) - 1 do
            local _, spellID = GetSpellBookItemInfo(j, "player")
            if spellID then
                tinsert(spells, spellID)
            end
        end
    end

    return spells[math.random(1, #spells)]
end

--------------------------------------------------------------------------------
-- Cast Bar Configuration
--------------------------------------------------------------------------------

function CastBar:SetDesiredWidth(width)
    self.sets.w = tonumber(width)
    self:Layout()
end

function CastBar:GetDesiredWidth()
    return self.sets.w or 240
end

function CastBar:SetDesiredHeight(height)
    self.sets.h = tonumber(height)
    self:Layout()
end

function CastBar:GetDesiredHeight()
    return self.sets.h or 32
end

-- font
function CastBar:SetFontID(fontID)
    self.sets.font = fontID
    self:SetProperty("font", self:GetFontID())

    return self
end

function CastBar:GetFontID()
    return self.sets.font or "Friz Quadrata TT"
end

-- texture
function CastBar:SetTextureID(textureID)
    self.sets.texture = textureID
    self:SetProperty("texture", self:GetTextureID())

    return self
end

function CastBar:GetTextureID()
    return self.sets.texture or "blizzard"
end

-- display
function CastBar:SetDisplay(part, enable)
    self.sets.display[part] = enable
    self:Layout()
end

function CastBar:Displaying(part)
    return self.sets.display[part]
end

-- latency padding
function CastBar:SetLatencyPadding(value)
    self.sets.latencyPadding = value
    self:Layout()
end

function CastBar:GetLatencyPadding()
    return self.sets.latencyPadding or tonumber(GetCVar("SpellQueueWindow")) or 0
end

function CastBar:SetUseSpellReactionColors(enable)
    if enable then
        self.sets.useSpellReactionColors = true
    else
        self.sets.useSpellReactionColors = false
    end

    self:UpdateColor()
end

function CastBar:UseSpellReactionColors()
    return self.sets.useSpellReactionColors
end

-- force the casting bar to show with the override ui/pet battle ui
function CastBar:ShowingInOverrideUI()
    return true
end

function CastBar:ShowingInPetBattleUI()
    return true
end

--------------------------------------------------------------------------------
-- Cast Bar Right Click Menu
--------------------------------------------------------------------------------

function CastBar:OnCreateMenu(menu)
    self:AddLayoutPanel(menu)
    self:AddDisplayPanel(menu)
    self:AddTexturePanel(menu)
    self:AddFontPanel(menu)

    menu:AddFadingPanel()

    menu:AddAdvancedPanel(true)

    menu:HookScript("OnShow", function()
        self.menuShown = true

        if not (self:GetProperty("state") == "casting" or self:GetProperty("state") == "channeling") then
            self:SetupDemo()
        end
    end)

    menu:HookScript("OnHide", function()
        self.menuShown = nil

        if self:GetProperty("state") == "demo" then
            self:Stop()
        end
    end)
end

function CastBar:AddLayoutPanel(menu)
    local panel = menu:NewPanel(LibStub("AceLocale-3.0"):GetLocale("Dominos-Config").Layout)

    panel.widthSlider = panel:NewSlider{
        name = L.Width,
        min = 1,
        max = function()
            return math.ceil(UIParent:GetWidth() / panel.owner:GetScale())
        end,
        get = function()
            return panel.owner:GetDesiredWidth()
        end,
        set = function(_, value)
            panel.owner:SetDesiredWidth(value)
        end
    }

    panel.heightSlider = panel:NewSlider{
        name = L.Height,
        min = 1,
        max = function()
            return math.ceil(UIParent:GetHeight() / panel.owner:GetScale())
        end,
        get = function()
            return panel.owner:GetDesiredHeight()
        end,
        set = function(_, value)
            panel.owner:SetDesiredHeight(value)
        end
    }

    panel:AddBasicLayoutOptions()
end

function CastBar:AddDisplayPanel(menu)
    local panel = menu:NewPanel(L.Display)

    panel:NewCheckButton{
        name = L["UseSpellReactionColors"],
        tooltip = L["UseSpellReactionColorsTip"],
        get = function()
            return panel.owner:UseSpellReactionColors()
        end,
        set = function(_, enable)
            panel.owner:SetUseSpellReactionColors(enable)
        end
    }

    for _, part in ipairs {"border", "icon", "spark", "time", "latency"} do
        panel:NewCheckButton{
            name = L["Display_" .. part],
            get = function()
                return panel.owner:Displaying(part)
            end,
            set = function(_, enable)
                panel.owner:SetDisplay(part, enable)
            end
        }
    end

    panel.latencySlider = panel:NewSlider{
        name = L.LatencyPadding,
        min = 0,
        max = function()
            return 500
        end,
        get = function()
            return panel.owner:GetLatencyPadding()
        end,
        set = function(_, value)
            panel.owner:SetLatencyPadding(value)
        end
    }
end

function CastBar:AddFontPanel(menu)
    local panel = menu:NewPanel(L.Font)

    panel.fontSelector = Dominos.Options.FontSelector:New{
        parent = panel,
        get = function()
            return panel.owner:GetFontID()
        end,
        set = function(_, value)
            panel.owner:SetFontID(value)
        end
    }
end

function CastBar:AddTexturePanel(menu)
    local panel = menu:NewPanel(L.Texture)

    panel.textureSelector = Dominos.Options.TextureSelector:New{
        parent = panel,
        get = function()
            return panel.owner:GetTextureID()
        end,
        set = function(_, value)
            panel.owner:SetTextureID(value)
        end
    }
end

-- exports
Addon.CastBar = CastBar
